package com.rhythm.commons.io;

import java.io.*;
import java.util.*;

/**
 * Utility class for I/O
 *
 **/
public class IOUtils {

    /**
     * The default buffer size to use.
     */
    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

    /**
     * Reads a file to a string
     * @param file file to read
     * @returns String contents of file
     **/
    public static String readFile(File file) throws IOException {
        FileReader filereader = null;
        String content = null;
        try {
            filereader = new FileReader(file);
            content = readReader(filereader);
            return content;
        } finally {
            closeSteamReader(filereader);
        }
    }

    /**
     * @param input stream to read
     * @returns String containing contents of file
     **/
    public static String readStream(InputStream input) throws IOException {
        return readReader(new InputStreamReader(input));
    }

    /**
     * @param input stream to read
     * @returns String containing contents of file
     **/
    public static String readReader(Reader input) throws IOException {
        try {
            StringBuffer buf = new StringBuffer();
            BufferedReader in = new BufferedReader(input);
            int ch;
            while ((ch = in.read()) != -1) {
                buf.append((char) ch);
            }
            return buf.toString();
        } finally {
            input.close();
        }
    }

    /**
     * Place a ".lock" file next to the given file. If one exists,
     * block (polling every second) until the existing lock
     * expires. If it's still there after 60 seconds, steal the
     * lock. <!-- Also keeps an in-memory monitor lock set as a backup. -->
     * @see #unlock(File)
     **/
    public static void lock(File f) throws IOException {
        File lock = lockfile(f);
        // System.err.println("locking " + lock);
        for (int i = 0; i < 60; ++i) {
            if (i == 59) {
                System.err.println("stealing lock on " + f);
                lock.delete();
                i = 0;
            }

            if (lock.createNewFile()) {
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
        // timestamp the lock
        PrintWriter out = new PrintWriter(new FileWriter(lock));
        out.println(System.currentTimeMillis());
        out.close();
    }

    /**
     * Removes the ".lock" file 
     * @see #lock(File)
     **/
    public static void unlock(File f) throws IOException {
        File lock = lockfile(f);
        lock.delete();
    }

    private static File lockfile(File f) {
        return new File(f.getParent(), f.getName() + ".lock");
    }

    /**
     * Prints a very simple log message to System.err
     **/
    public static void log(String s) {
        s = new Date().toString() + ": " + s;
        System.err.println(s);
    }

    public static Properties loadProperties(String filename) throws IOException {
        return loadProperties(new File(filename));
    }

    public static Properties loadProperties(File file) throws IOException {
        Properties properties = null;
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            properties = new Properties();
            properties.load(in);
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
            }
        }
        return properties;
    }

    /** 
     * Write the input stream to the file as raw data
     * @param dir directory to put the file in
     * @param name name of new file
     * @param data the data to write in the file
     **/
    public static void writeFile(File dir, String name, InputStream data) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        try {
            File outFile = new File(dir, name);
            in = new BufferedInputStream(data);
            out = new BufferedOutputStream(new FileOutputStream(outFile));
            int ch;
            while ((ch = in.read()) != -1) {
                out.write(ch);
            }
        } catch (IOException ioe) {
            log("Error in writeFile: " + ioe);
            ioe.printStackTrace();
            throw ioe;
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
            }
        }
    } // writeFile

    /** 
     * Write the input string to the file as raw data
     * @param dir directory to put the file in
     * @param name name of new file
     * @param data the data to write in the file
     **/
    public static void writeString(File dir, String name, String data) throws IOException {
        writeString(new File(dir, name), data);
    }

    /** 
     * Write the input string to the file as raw data
     * @param name name of new file
     * @param data the data to write in the file
     **/
    public static void writeString(File file, String data) throws IOException {
        PrintWriter out = null;
        FileOutputStream outputStream = new FileOutputStream(file, true);
        try {

            out = new PrintWriter(outputStream);
            out.print(data);
        //out.print(data);
        } finally {
            outputStream.flush();
            //	    try {
            if (out != null) {
                out.close();
            }
        //        } catch (IOException e) {}
        }
    }

    /** 
     * Write the properties parameter to the file as raw data
     * @param dir directory to put the file in
     * @param name name of new file
     * @param data the data to write in the file
     **/
    public static void writeProperties(File dir, String name, Properties prop) throws IOException {
        File file;
        FileOutputStream out = null;
        try {
            file = new File(dir, name);
            out = new FileOutputStream(file);
            prop.store(out, "properties for " + name + " written " + new Date());
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
            }
        }
    }

    /**
     * Removes "./" and "foo/../" and trailing "/" from pathname
     * turns \ (backslash) into / (forward slash)
     */
    public static String fixPath(String path) throws IOException {
        Stack stack = getPathStack(path);

        // turn stack into path
        StringBuffer fixed = new StringBuffer(path.length());
        Iterator i = stack.iterator();
        boolean first = true;
        while (i.hasNext()) {
            String s = (String) i.next();
            if (first) {
                first = false;
            } else {
                fixed.append("/");
            }
            fixed.append(s);
        }
        return fixed.toString();
    }

    /**
     * Given a filesystem path with slashes, turns it into a stack,
     * where each entry on the stack is a directory in the path.
     * Deals properly with "./" and "../" (in the former case, it
     * ignores it; in the latter case, it pops the previous entry) so
     * all you're left with is valid directories.  Does not deal with
     * symlinks etc.
     **/
    public static Stack getPathStack(String path) throws IOException {
        // turn it into a list
        StringTokenizer tok = new StringTokenizer(path, "/\\");
        Stack stack = new Stack();
        String s = "";
        try {
            while (tok.hasMoreTokens()) {
                s = tok.nextToken();
                if (s.equals(".")) {
                    continue;
                } else if (s.equals("..")) {
                    stack.pop();
                } else {
                    stack.push(s);
                }
            }
        } catch (EmptyStackException e) {
            throw new IOException("Bad path " + path + " - too many ..s");
        }
        return stack;
    }

    /**
     * compares the contents of two files
     * @return true if the two files differ at any point
     **/
    public static boolean isDiff(File a, File b) throws IOException {
        InputStream inA = null, inB = null;
        try {
            inA = new BufferedInputStream(new FileInputStream(a));
            inB = new BufferedInputStream(new FileInputStream(b));
            return isDiff(inA, inB);
        } finally {
            try {
                if (inA != null) {
                    inA.close();
                }
            } finally {
                if (inB != null) {
                    inB.close();
                }
            }
        }
    }

    /**
     * compares the contents of two streams
     * @return true if the two streams differ at any point
     **/
    public static boolean isDiff(InputStream a, InputStream b) throws IOException {
        int x, y;

        do {
            x = a.read();
            y = b.read();
            // if a byte doesn't correspond, or if one ends before the
            // other, they're different
            if (x != y) {
                return true;
            }

        } while (x != -1);
        return false;
    }

    /**
     * Copy bytes from an <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     * <p>
     * Large streams (over 2GB) will return a bytes copied value of
     * <code>-1</code> after the copy has completed since the correct
     * number of bytes cannot be returned as an int. For large streams
     * use the <code>copyLarge(InputStream, OutputStream)</code> method.
     *
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @return the number of bytes copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @throws ArithmeticException if the byte count is too large
     */
    public static int copy(InputStream input, OutputStream output) throws IOException {
        long count = copyLarge(input, output);
        if (count > Integer.MAX_VALUE) {
            return -1;
        }
        return (int) count;
    }

    /**
     * Copy bytes from a large (over 2GB) <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     *
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @return the number of bytes copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     */
    public static long copyLarge(InputStream input, OutputStream output)
            throws IOException {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        long count = 0;
        int n = 0;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }

    /**
     * Copies one file to another. NEW CODE -- NOT YET TESTED
     **/
    public static void copyFile(File source, File target) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = new BufferedInputStream(new FileInputStream(source));
            out = new BufferedOutputStream(new FileOutputStream(target));
            int ch;
            while ((ch = in.read()) != -1) {
                out.write(ch);
            }
            out.flush();	// just in case
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ioe) {
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ioe) {
                }
            }
        }
    }

    public static void chmod(File file, String mode) throws IOException {
        Runtime.getRuntime().exec(new String[]{"chmod", mode, file.getAbsolutePath()});
    }

    public static void closeSteamReader(InputStreamReader streamReader) throws IOException {
        if (streamReader != null) {
            streamReader.close();
        }
    }

        /**
     * Unconditionally close an <code>Reader</code>.
     * <p>
     * Equivalent to {@link Reader#close()}, except any exceptions will be ignored.
     * This is typically used in finally blocks.
     *
     * @param input  the Reader to close, may be null or already closed
     */
    public static void closeQuietly(Reader input) {
        try {
            if (input != null) {
                input.close();
            }
        } catch (IOException ioe) {
            // ignore
        }
    }

        /**
     * Unconditionally close a <code>Writer</code>.
     * <p>
     * Equivalent to {@link Writer#close()}, except any exceptions will be ignored.
     * This is typically used in finally blocks.
     *
     * @param output  the Writer to close, may be null or already closed
     */
    public static void closeQuietly(Writer output) {
        try {
            if (output != null) {
                output.close();
            }
        } catch (IOException ioe) {
            // ignore
        }
    }

        /**
     * Unconditionally close an <code>InputStream</code>.
     * <p>
     * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
     * This is typically used in finally blocks.
     *
     * @param input  the InputStream to close, may be null or already closed
     */
    public static void closeQuietly(InputStream input) {
        try {
            if (input != null) {
                input.close();
            }
        } catch (IOException ioe) {
            // ignore
        }
    }

        /**
     * Unconditionally close an <code>OutputStream</code>.
     * <p>
     * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored.
     * This is typically used in finally blocks.
     *
     * @param output  the OutputStream to close, may be null or already closed
     */
    public static void closeQuietly(OutputStream output) {
        try {
            if (output != null) {
                output.close();
            }
        } catch (IOException ioe) {
            // ignore
        }
    }
}
